/*
 * Copyright (C) 2011 Michael Widmer <widmm8@bfh.ch>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package ch.bfh.aeneas.navigation.app;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import ch.bfh.aeneas.exchange.intent.AeneasIntent;
import ch.bfh.aeneas.exchange.intent.MalformedIntentException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This class figures as the base class for most of the navigation services.
 * It mainly offers the possibilities to start requests and managing them. 
 * Furthermore it also handle communication with NavigationActivity by offering
 * various methods to unicast state changes.
 * 
 * @see NavigationActivity
 * @author Michael Widmer <widmm8@bfh.ch>
 */
public abstract class NavigationService extends Service {
    
    /**
     * Components interested in status updates of this Service should be
     * implementing this interface and register themself on this service by
     * calling addOnStateListener.
     * Beware: This interface is not mandatory and so you may never receive any
     * updates, if service doesn't support this interface.
     * @see #addOnStateListener(ch.bfh.aeneas.navigation.app.NavigationService.OnStateListener)
     * @see #removeOnStateListener(ch.bfh.aeneas.navigation.app.NavigationService.OnStateListener) 
     */
    public interface OnStateListener {
        
        /**
         * Gets called before service initializes. (Not sure to get called!)
         */
        public void onBeforeInit();
        /**
         * Gets called after service initialized. (Not sure to get called!)
         */
        public void onAfterInit();
        /**
         * Gets called as soon as the service prepared all his needs and is
         * ready to get worked on. (Not sure to get called!)
         */
        public void onReady();
    }   
    
    /**
     * Components interested in results of requests on providers should 
     * implement this interface and register themself on this service.
     * @see #addOnResultsListener(ch.bfh.aeneas.navigation.app.NavigationService.OnResultsListener) 
     * @see #removeOnResultsListener(ch.bfh.aeneas.navigation.app.NavigationService.OnResultsListener) 
     */
    public interface OnResultsListener {
        public void onResults(int request, Intent i);
        public void onRequestSent(int request);
        public void onRequestClosed(int request);
    }
    
    /**
     * This a BroadcastReceiver to get answers from providers. Use this instance
     * to get inform about newly received answers through OnResultListener-
     * Interface.
     * @see OnResultsListener
     */
    protected BroadcastReceiver mAnswerListener = new BroadcastReceiver() {
        @Override
        public void onReceive(Context arg0, Intent arg1) {
            try {
                Log.i(LOG_TAG, "Received answer... trying to handle");
                handleAnswer(arg1);
            } catch(Exception ex) {
                Log.w(LOG_TAG, "any unknown error happened: "+ex.getMessage());
            }
        }        
    };
    
    /**
     * This is the base class to enforce the timeout of requests. As you can't 
     * cancel currently executed tasks on providers you may ignore their answers
     * after timeout. For each intent sent by broadcast a corresponding
     * CloseReceiver is created.
     * @see #broadcast(android.content.Intent, long) 
     */
    protected class CloseReceiver implements Runnable {
        private int id;
        
        /**
         * @param identifier The id of the request to handle.
         */
        public CloseReceiver(int identifier) {
            id = identifier;
        }
        
        @Override
        /**
         * {@inheritDoc}
         */
        public void run() {
            Log.i(LOG_TAG, "RUN updater-thread");
            mCloseTable.remove(id);
            mCloseTable.put(id, false);
            
            onRequestClosed(id);
            
            informOnRequestFinished(id);
            
            mTimer.removeCallbacks(this);
            mCloseTimers.remove(this);
        }
    }
    
    public class NavigationServiceBinder extends Binder {
        public NavigationService getService() {
            return NavigationService.this;
        }
    }
    private final NavigationServiceBinder mBinder = new NavigationServiceBinder();
    
    /**
     * A shortcut to use when asking for requestId on intents.
     */
    protected static final String REQUEST_ID = "requestId";
    
    private String LOG_TAG = "NavigationService";    
    private boolean mReady;
    private Handler mTimer;
    private Map<Integer, Boolean> mCloseTable;
    private List<Integer> mRequestIds;
    private List<CloseReceiver> mCloseTimers;
    private List<OnResultsListener> mResultsListener;
    private List<OnStateListener> mStateListener;
    
    @Override
    /**
     * {@inheritDoc}
     */
    public void onCreate() {
        super.onCreate();
        //mTimer = new Timer();
        mTimer = new Handler();
        mCloseTable = new HashMap();
        mCloseTimers = new ArrayList<CloseReceiver>();
        mRequestIds = new ArrayList<Integer>();
        mResultsListener = new ArrayList<OnResultsListener>();
        mStateListener = new ArrayList<OnStateListener>();
    }
    
    @Override
    /**
     * {@inheritDoc}
     */
    public IBinder onBind(Intent intent) {
        Log.i(LOG_TAG, "onBind!");
        registerReceiver(mAnswerListener,new IntentFilter(AeneasIntent.ACTION_RESPONSE));        
        return mBinder;
    }
    
    @Override
    /**
     * {@inheritDoc}
     */
    public boolean onUnbind(Intent i) {
        Log.i(LOG_TAG, "onUnbind!");
        unregisterReceiver(mAnswerListener);
        super.onUnbind(i);
        return true;
    }
    
    @Override
    /**
     * {@inheritDoc}
     */
    public void onDestroy() {
        CloseReceiver cr;
        for(int i = 0; i < this.mCloseTimers.size(); i++) {
            cr = mCloseTimers.get(i);
            mTimer.removeCallbacks(cr);
        }        
        super.onDestroy();
    }
    
    /**
     * Add a new listener to get updates on new answers received from providers.
     * @param listener 
     */
    public void addOnResultsListener(OnResultsListener listener) {
        if(!mResultsListener.contains(listener)) {
            mResultsListener.add(listener);
        }
    }
    
    /**
     * Removes the listener if previously registered.
     * @param listener 
     */
    public void removeOnResultsListener(OnResultsListener listener) {
        if(mResultsListener.contains(listener)) {
            mResultsListener.remove(listener);
        }
    }
    
    /**
     * Informs all registered listeners that a new answer from providers has
     * arrived.
     * @param intent The answer received from provider
     * @see OnResultsListener
     */
    protected void informOnResult(Intent intent) {
        Log.i(LOG_TAG, "Informing listeners about received result");
        if(intent.hasExtra("requestId")) {
            
            int requestId = intent.getIntExtra("requestId", -1);
            
            for(int i = 0; i < mResultsListener.size(); i++) {
                mResultsListener.get(i).onResults(requestId, intent);
            }
        }
    }
    
    /**
     * Informs all registered listeners that a request is about to be sent.
     * @param request The id of the request that will get sent.
     * @see OnResultsListener
     */
    protected void informOnRequestStarted(int request) {
        for(int i = 0; i < mResultsListener.size(); i++) {
            mResultsListener.get(i).onRequestSent(request);
        }
    }
    
    /**
     * Informs all registered listeners that a request has been finished after
     * reaching timeout.
     * @param request The id of the request that finished.
     * @see OnResultsListener
     */
    protected void informOnRequestFinished(int request) {
        Log.i(LOG_TAG, "Informing listeners about finished request");
        for(int i = 0; i < mResultsListener.size(); i++) {
            mResultsListener.get(i).onRequestClosed(request);
        }
    }
    
    /**
     * Adds a new listener to get informed about state changes (if any dispatched).
     * @param listener 
     * @see OnStateListener
     */
    public void addOnStateListener(OnStateListener listener) {
        if(!mStateListener.contains(listener)) {
            mStateListener.add(listener);
            if(mReady) {
                listener.onReady();
            }
        }
    }
    
    /**
     * Removes the passed listener, if registered.
     * @param listener 
     * @see OnStateListener
     */
    public void removeOnStateListener(OnStateListener listener) {
        if(mStateListener.contains(listener)) {
            mStateListener.remove(listener);
        }
    }
    
    /**
     * Informs all registered listeners that service is just about to start 
     * initialization.
     * @see OnStateListener
     */
    protected void informOnBeforeInit() {
        Log.i(LOG_TAG, "Informing listeners about before init");
        for(int i = 0; i < mStateListener.size(); i++) {
            mStateListener.get(i).onBeforeInit();
        }
    }
    
    /**
     * Informs all registered listeners that service has just been initialized.
     * @see OnStateListener
     */
    protected void informOnAfterInit() {
        Log.i(LOG_TAG, "Informing listeners about after init");
        for(int i = 0; i < mStateListener.size(); i++) {
            mStateListener.get(i).onAfterInit();
        }
    }
    
    /**
     * Informs all registered listeners that service is now ready to work with.
     * @see OnStateListener
     */
    protected void informOnReady() {
        Log.i(LOG_TAG, "Informing listeners about ready");
        for(int i = 0; i < mStateListener.size(); i++) {
            mStateListener.get(i).onReady();
        }
    }
    
    /**
     * Returns a boolean indication if service is ready (true) or not (false).
     * @return 
     * @see OnStateListener
     */
    public boolean isReady() {
        return mReady;
    }
    
    /**
     * Set this service's state to ready, meaning switching field mReady to true
     * and informing registered listeners.
     * @see #isReady() 
     */
    protected void setReady() {
        mReady = true;
        this.informOnReady();
    }
    
    /**
     * A shortcut to get a specific intent equipped with a requestId that gets 
     * stored to handle later actions such as starting and finishing requests.
     * @param action The action for the intent
     * @return An intent containing the specified action
     * @see AeneasIntent
     */
    protected AeneasIntent getBroadcastIntent(String action) {
        AeneasIntent i = new AeneasIntent(action);
        mRequestIds.add(i.getRequestId());
        return i;
    }
    
    /**
     * A shortcut to get a specific intent equipped with a requestId that gets 
     * stored to handle later actions such as starting and finishing requests.
     * @param action The action for the intent
     * @param category The category for the intent
     * @return An intent containing the specified action and category
     * @see AeneasIntent
     */
    protected AeneasIntent getBroadcastIntent(String action, String category) {
        AeneasIntent i = this.getBroadcastIntent(action);
        i.addCategory(category);
        return i;
    }
    
    /**
     * A shortcut to get a specific intent equipped with a requestId that gets 
     * stored to handle later actions such as starting and finishing requests.
     * @return An intent containing 'request' as action
     * @see AeneasIntent
     * @see AeneasIntent#ACTION_REQUEST
     */
    protected AeneasIntent getBroadcastIntent() {
        return getBroadcastIntent(AeneasIntent.ACTION_REQUEST);
    }
    
    /**
     * Sends the passed intent via broadcast to listening BroadcastReceivers.
     * The parameter duration specifies the timeout for the CloseReceiver 
     * handling this request, meaning that answers arriving after this time will
     * be ignored.
     * 
     * If you pass an intent without an extra 'requestId' a 
     * MalformedIntentException will be thrown.
     * @param i The request to send
     * @param duration The timeout after which answers will be ignores
     * @throws MalformedIntentException Gets thrown, if passe intent doesn't contain an extra named 'requestId'
     */
    protected final void broadcast(Intent i, long duration) throws MalformedIntentException {
        if(!i.hasExtra(REQUEST_ID)) {
            throw new MalformedIntentException(MalformedIntentException.TYPE_MISSING, REQUEST_ID);
        }
        int request = i.getIntExtra(REQUEST_ID, -1);
        mCloseTable.put(request, true);
        //mTimer.schedule(new CloseReceiver(request), duration);
        
        CloseReceiver closer = new CloseReceiver(request);
        mCloseTimers.add(closer);
        mTimer.postDelayed(closer, duration);
        
        Log.i(LOG_TAG, "sending broadcast of type: "+i.getAction()+" with id "+request+" and timeout "+duration);
        informOnRequestStarted(request);
        getApplicationContext().sendBroadcast(i);
    }
    
    /**
     * Checks if an intent with specified id has been requested by this service.
     * @param id The id of the request
     * @return Boolean value indication if intent was requested by this service
     */
    protected boolean isOwnRequest(int id) {
        boolean isRequest = mRequestIds.contains(id);
        Log.i(LOG_TAG, "Is own request? "+isRequest);
        Log.i(LOG_TAG, "Checking for id "+id+" inside: "+mRequestIds.toString());
        return isRequest;
    }
    
    /**
     * Checks if the incoming intent is an answer onto an intent that was
     * requested by this service. If the intent is an answer to one of the own
     * requests it is going to passed to onAnswer for getting handled in 
     * possible deriving classes.
     * @param i The answer that arrived
     * @see #isOwnRequest(int)
     * @see #onAnswer(android.content.Intent) 
     */
    private void handleAnswer(Intent i) {
        Log.i(LOG_TAG, "Received answer!");
        Bundle extras = i.getExtras();
        if(extras != null && extras.containsKey(REQUEST_ID)) {
            int requestId = extras.getInt(REQUEST_ID);
            Log.i(LOG_TAG, "Looking up for request-id: "+requestId);
            if(isOwnRequest(requestId) && mCloseTable.get(requestId)) {
                onAnswer(i);
            }
        } else {
            Log.i(LOG_TAG,"Sorry! extras was null or there was no request id!");
        }
    }
    
    /**
     * Gets called, when the timeout for the corresponding request is over. 
     * @param id The request whose timeout just passed
     * @see CloseReceiver
     * 
     */
    protected void onRequestClosed(int id) {};
    
    /**
     * Gets called, when an answer to an request of the services own arrives. 
     * @param i The answer that arrived
     */
    protected abstract void onAnswer(Intent i);
}
